Skip to content

Support passing preimage for spontaneous payments #549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

aagbotemi
Copy link
Contributor

This PR adds support for specifying custom preimages when sending spontaneous (keysend) payments.

Solution

  • Added send_with_preimage() method
  • Added send_with_preimage_and_custom_tlvs() method for advanced use cases
  • Modified internal send_inner() to accept optional preimage parameter and generate random preimage only when custom preimage is not provided.

An integration test is added to verify logic, and the new methods are added to the bindings.

Fixes #535

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented May 31, 2025

👋 I see @TheBlueMatt was un-assigned.
If you'd like another reviewer assignemnt, please click here.

Copy link

@chuksys chuksys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just shared a few thoughts regarding the overall design of this API and its extensibility.

/// Send a spontaneous with custom preimage
pub fn send_with_preimage(
&self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option<SendingParameters>,
preimage: PaymentPreimage,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should expand the purpose of SendingParameters a bit to allow passing of any custom parameters required for sending payments (not just routing and pathfinding parameters).

This approach would allow us to include preimage as an Option within SendingParameters, providing a single, coherent place for all payment-sending customizations.

A significant benefit of this change would be the ability to deprecate or drop specialized functions like send_with_preimage and send_with_custom_tlvs (in another PR), reducing overall boilerplate for users.

What do you think about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially hesitated about mixing routing parameters with payment content. Your point about expanding SendingParameters to handle all payment customization makes sense for the long-term API design, and the benefit outweigh the conceptual mixing.

I'll refactor to move preimage into SendingParameters as an Option. I can drop the send_with_preimage and send_with_preimage_and_custom_tlvs, then send_with_custom_tlvs can be dropped in another PR.

Thank you.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should expand the purpose of SendingParameters a bit to allow passing of any custom parameters required for sending payments (not just routing and pathfinding parameters).

Ah, no, I don't think we should do that. Note that after LDK 0.2 release (cf. #462) we'll replace SendingParameters with LDK's RouteParametersConfig everywhere, so really shouldn't make any changes prior to it. Also, while we might be fine to expose the preimage for keysend payments, I don't think we'd like to allow users to override it for BOLT11/BOLT12 generally.

I'll refactor to move preimage into SendingParameters as an Option.

Please revert this. I think we can either have separate send APIs as you previously suggested, or add the preimage as an optional field to the existing ones. Will take a look after the revert, but your previous approach might be fine.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no, I don't think we should do that. Note that after LDK 0.2 release (cf. #462) we'll replace SendingParameters with LDK's RouteParametersConfig everywhere, so really shouldn't make any changes prior to it. Also, while we might be fine to expose the preimage for keysend payments, I don't think we'd like to allow users to override it for BOLT11/BOLT12 generally.

Thanks for the clarification!

self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs), None)
}

/// Send a spontaneous with custom preimage
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you missed the word "payment" here (after spontaneous) - that's if we'd still be adding this method.

Copy link
Contributor Author

@aagbotemi aagbotemi Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I missed that. Thank you. I am removing the method.

Copy link

@chuksys chuksys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to use sending_parameters to pass in the custom preimage, I think we should allow the user to set it in config or pass it directly.

@@ -77,7 +77,10 @@ impl SpontaneousPayment {
return Err(Error::NotRunning);
}

let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes());
let payment_preimage = sending_parameters
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are doing this, I think we'll need to use override_params instead as that allows send_inner to select sending_parameters set in the config (if not passed into the send method directly).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think we should do this, see above.

@tnull tnull removed the request for review from TheBlueMatt June 2, 2025 11:15
Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above I don't think adding this field to SendingParameters is the way to go here (note it would also allowing setting global default preimage if users would override the field in Config, which is actually dangerous as preimages can never be reused). Please revert to you previous draft, will review then.

@@ -77,7 +77,10 @@ impl SpontaneousPayment {
return Err(Error::NotRunning);
}

let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes());
let payment_preimage = sending_parameters
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think we should do this, see above.

@aagbotemi
Copy link
Contributor Author

Please revert to you previous draft, will review then.

Alright. Will do that. Thank you.

@aagbotemi
Copy link
Contributor Author

Please revert to you previous draft, will review then.

Hello @tnull, I have reverted to the previous draft. This is ready.

@Camillarhi
Copy link
Contributor

Camillarhi commented Jun 2, 2025

Nice work on this PR! The custom preimage functionality is implemented cleanly, and the test coverage looks good.

One small enhancement suggestion: we could add verification that node_b received the payment to complete end-to-end verification of the payment flow.

@aagbotemi
Copy link
Contributor Author

One small enhancement suggestion: we could add verification that node_b received the payment to complete end-to-end verification of the payment flow.

Thanks for the suggestion! I've added the end-to-end verification as requested. It also ensure the custom preimage flows correctly through the entire payment process from sender to receiver.

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically LGTM, but now that we actually take a PaymentPreimage from user input we should probably avoid the UniffiCustomTypeConverter for it, and rather expose a 'proper' newtype wrapper for the bindings. See #542 for inspiration on this, for example.

/// Send a spontaneous payment with custom preimage
pub fn send_with_preimage(
&self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option<SendingParameters>,
preimage: PaymentPreimage,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add this as a new required parameter, let's move it before the optional parameters, i.e., Option<SendingParameters> (here and below).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, will fix it. Thank you.

@aagbotemi
Copy link
Contributor Author

Basically LGTM, but now that we actually take a PaymentPreimage from user input we should probably avoid the UniffiCustomTypeConverter for it, and rather expose a 'proper' newtype wrapper for the bindings. See #542 for inspiration on this, for example.

Thank you for the feedback, I will fix it

@aagbotemi aagbotemi force-pushed the support-preimage-spontaneous-payment branch 2 times, most recently from 2ffdc53 to 5194ccc Compare June 25, 2025 21:04
Copy link

@chuksys chuksys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good to me - would appreciate some clarity on why we have maybe_wrap and maybe_wrap_arc.

src/ffi/mod.rs Outdated
Comment on lines 38 to 46
pub fn maybe_wrap_arc<T>(ldk_type: impl Into<T>) -> std::sync::Arc<T> {
std::sync::Arc::new(ldk_type.into())
}

#[cfg(feature = "uniffi")]
pub fn maybe_wrap<T>(ldk_type: impl Into<T>) -> T {
ldk_type.into()
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably missing something here but I'm wondering why we need to "split" maybe_wrap into these 2 functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe_wrap and maybe_wrap_arc is to handle different type wrapping requirements depending on whether the uniffi feature is enabled. maybe_wrap_arc wraps the input type in a std::sync::Arc<T>. maybe_wrap performs a simple into() conversion without additional wrapping, used in cases where the type doesn’t need to be wrapped in an Arc.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, I thought maybe_wrap (as it was before) already handled wrapping depending on whether the uniFFI feature flag is enabled - if enabled wrap in an Arc, if not return same value as passed in?

I noticed maybe_wrap (this new version) is used to conditionally wrap the preimage - does this mean the preimage never needs to be wrapped in an Arc even when the uniFFI feature flag is enabled?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #542, the wrapper types introduced were for structs with a bit more complexity and state than PaymentPreimage. From my understanding, it makes sense to allocate them on the heap because they are also expensive to clone. PaymentPreimage is simpler, wraps 32 bytes, and should be cheaper to clone and doesn't need to be Arc'ed.

Also, when compiled with uniffi, there was a bit of conflict about converting maybe_wrap into a macro so it can handle the different cases (i.e. types needing heap allocation vs types that do not), however I think @tnull may not like the idea for reasons stated in #526.

This change is easily revertible though if Arcing is preferred.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #542, the wrapper types introduced were for structs with a bit more complexity and state than PaymentPreimage. From my understanding, it makes sense to allocate them on the heap because they are also expensive to clone. PaymentPreimage is simpler, wraps 32 bytes, and should be cheaper to clone and doesn't need to be Arc'ed.

Thanks for the clarification! This makes sense.

src/ffi/mod.rs Outdated
Comment on lines 57 to 47
#[cfg(not(feature = "uniffi"))]
pub fn maybe_wrap_arc<T>(value: T) -> T {
value
}

#[cfg(not(feature = "uniffi"))]
pub fn maybe_wrap<T>(value: T) -> T {
value
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 fns are basically the same, please is there a reason why we need both of them?

Copy link
Contributor Author

@aagbotemi aagbotemi Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both maybe_wrap_arc and maybe_wrap are identical, simply returning the input value. The reason for keeping them as separate functions is to maintain a consistent API that aligns with their distinct behaviors when the uniffi feature is enabled

@aagbotemi
Copy link
Contributor Author

Hi @tnull @enigbe. I pushed some update that expose a 'proper' newtype wrapper for the bindings.

src/ffi/mod.rs Outdated
Comment on lines 38 to 46
pub fn maybe_wrap_arc<T>(ldk_type: impl Into<T>) -> std::sync::Arc<T> {
std::sync::Arc::new(ldk_type.into())
}

#[cfg(feature = "uniffi")]
pub fn maybe_wrap<T>(ldk_type: impl Into<T>) -> T {
ldk_type.into()
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #542, the wrapper types introduced were for structs with a bit more complexity and state than PaymentPreimage. From my understanding, it makes sense to allocate them on the heap because they are also expensive to clone. PaymentPreimage is simpler, wraps 32 bytes, and should be cheaper to clone and doesn't need to be Arc'ed.

Also, when compiled with uniffi, there was a bit of conflict about converting maybe_wrap into a macro so it can handle the different cases (i.e. types needing heap allocation vs types that do not), however I think @tnull may not like the idea for reasons stated in #526.

This change is easily revertible though if Arcing is preferred.

@@ -446,6 +450,10 @@ dictionary PaymentDetails {
u64 latest_update_timestamp;
};

dictionary PaymentPreimage {
bytes inner;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about what would be the most appropriate description for [u8;N]: bytes vs sequence<u8>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytes represents fixed-size binary data like preimage better

@enigbe
Copy link
Contributor

enigbe commented Jun 26, 2025

@aagbotemi
Just added a couple of comments.
You'll need to give CI another kick to get the failing Rust Tests to pass.

@aagbotemi aagbotemi force-pushed the support-preimage-spontaneous-payment branch from 68a141d to c33d291 Compare June 26, 2025 13:53
Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When migrating PaymentPreimage to use its own wrapper type, it's important to just use this type in the Uniffi API. It should just wrap the inner type and not add any additional logic that might get stale over time. In particular, we def. shouldn't add specific serialization logic, as we'd still hold LdkPaymentPreimage internally, and just convert it at the API boundaries if necessary. To this end, please try to avoid adding more conversion wrapper methods if they are not really necessary (and I don't think they are in this case?).

It would be great if you could split this PR in two commits: a first commit introducing the Uniffi wrapper for PaymentPreimage, the second one exposing the new APIs for spontaneous payments.

Lastly, please try to write more conclusive commit messages that describe the changes, but also provide rationale for the changes. Feel free to refer to https://cbea.ms/git-commit/ for some guidance here. Thanks!

src/ffi/types.rs Outdated
type Builtin = String;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaymentPreimage {
pub(crate) inner: Vec<u8>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency reasons, please just store an LdkPaymentPreimage as an inner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. Thank you

src/ffi/types.rs Outdated
}
}

impl Readable for PaymentPreimage {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we really shouldn't roll our own custom serialization PaymentPreimage here, we just use the wrapper type in the API, for nothing else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. Noted

src/ffi/mod.rs Outdated
@@ -18,6 +18,14 @@ where
wrapped_type.as_ref().as_ref()
}

#[cfg(feature = "uniffi")]
pub fn maybe_extract<T, R>(wrapped_type: T) -> Result<R, crate::error::Error>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this, as we could just reuse maybe_try_convert_enum (which could be renamed to maybe_try_from, maybe).

In general, I'd prefer it if we wouldn't add any more wrapper helpers in this PR, I think (?) we should be able to make it work with what's there, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will make use of the wrapper helper we currently have.

@aagbotemi
Copy link
Contributor Author

When migrating PaymentPreimage to use its own wrapper type, it's important to just use this type in the Uniffi API. It should just wrap the inner type and not add any additional logic that might get stale over time. In particular, we def. shouldn't add specific serialization logic, as we'd still hold LdkPaymentPreimage internally, and just convert it at the API boundaries if necessary. To this end, please try to avoid adding more conversion wrapper methods if they are not really necessary (and I don't think they are in this case?).

It would be great if you could split this PR in two commits: a first commit introducing the Uniffi wrapper for PaymentPreimage, the second one exposing the new APIs for spontaneous payments.

Lastly, please try to write more conclusive commit messages that describe the changes, but also provide rationale for the changes. Feel free to refer to https://cbea.ms/git-commit/ for some guidance here. Thanks!

Thank you for the feedback @tnull. I will address the comments and push the updated changes.

@enigbe
Copy link
Contributor

enigbe commented Jul 1, 2025

When migrating PaymentPreimage to use its own wrapper type, it's important to just use this type in the Uniffi API. It should just wrap the inner type and not add any additional logic that might get stale over time. In particular, we def. shouldn't add specific serialization logic, as we'd still hold LdkPaymentPreimage internally, and just convert it at the API boundaries if necessary.

@aagbotemi initially tried this approach, representing PaymentPreimage as an interface/object that wraps LdkPaymentPreimage. However, because the LightningBalance enum has a variant struct with PaymentPreimage field, we are unable to just wrap the inner type as is because there is no support for interfaces/objects in an enum.

thread 'main' panicked at build.rs:10:59:
  called `Result::unwrap()` on an `Err` value: Objects cannot currently be used in enum variant data
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

I recommended representing PaymentPreimage as a dictionary to circumvent this error, but that means we have to add specific serialization for this, as has been done.

We are currently using

uniffi = { version = "0.27.3", features = ["build"], optional = true }

and this error originates from this:

# uniffi_udl-0.27.3/src/converters/callables.rs
impl APIConverter<FieldMetadata> for weedle::argument::SingleArgument<'_> {
    fn convert(&self, ci: &mut InterfaceCollector) -> Result<FieldMetadata> {
        let type_ = ci.resolve_type_expression(&self.type_)?;
        if let Type::Object { .. } = type_ {
            bail!("Objects cannot currently be used in enum variant data");
        }
        if self.default.is_some() {
            bail!("enum interface variant fields must not have default values");
        }
        if self.attributes.is_some() {
            bail!("enum interface variant fields must not have attributes");
        }
        // TODO: maybe we should use our own `Field` type here with just name and type,
        // rather than appropriating record::Field..?
        Ok(FieldMetadata {
            name: self.identifier.0.to_string(),
            ty: type_,
            default: None,
            docstring: None,
        })
    }
}

but may have been addressed in "0.29.3".

@tnull any reservations about updating uniffi to explore this? any suggestions on how to handle this without additional logic is welcome.

@tnull
Copy link
Collaborator

tnull commented Jul 1, 2025

However, because the LightningBalance enum has a variant struct with PaymentPreimage field, we are unable to just wrap the inner type as is because there is no support for interfaces/objects in an enum.

Ah, no, I think we are able to go this way, but it unfortunately mean we'll have to create a bindings-specific variant of LightningBalance that holds the respective field in an Arc and switch it around based on the uniffi-feature, too.

but may have been addressed in "0.29.3".

Good to know!

@tnull any reservations about updating uniffi to explore this? any suggestions on how to handle this without additional logic is welcome.

Yeah, unfortunately we can't upgrade to 0.29.3: for one, it's MSRV is 1.82, while we're on 1.75. But, the more important aspect is that we have users that use LDK Node in conjuction with uniffi-bindgen-go, which is only compatible with 0.28 currently. I.e., if we upgraded and made use of newer feature, they'd need to downgrade and apply the same workarounds in their fork, which I'd like to avoid (cc @rolznz). But we should be able to upgrade to 0.28 by now, which we now do in #591, even though this would likely not make our lives easier in this PR, sadly.

@enigbe
Copy link
Contributor

enigbe commented Jul 2, 2025

Ah, no, I think we are able to go this way, but it unfortunately mean we'll have to create a bindings-specific variant of LightningBalance that holds the respective field in an Arc and switch it around based on the uniffi-feature, too.

Thanks for this pointer.

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, sorry, this PR fell a bit off my radar and up until recently I wasn't aware that it was still blocked on the best way of exposing a PaymentPreimage object in Uniffi bindings.

Given that it's been open for a while now, I'd suggest to decouple the two tasks after all to make some progress. I.e., let's just revert this PR to have only the changes to 'support passing preimage for spontaneous payments', and then we can see whether/how we can expose the PaymentPreimage better/safer in a follow-up.

FWIW, we might actually punt on it until we can upgrade to uniffi 0.29 given that it seems it would make our lives considerably easier, as @enigbe suggested above. (which would be the case once uniffi-bindgen-go makes a move on their pending upgrade PR. Note that we're about to bump MSRV with #606).

@aagbotemi
Copy link
Contributor Author

I'd suggest to decouple the two tasks after all to make some progress. I.e., let's just revert this PR to have only the changes to 'support passing preimage for spontaneous payments',

Alright, I'll revert the PR. Thank you.

@aagbotemi aagbotemi force-pushed the support-preimage-spontaneous-payment branch 3 times, most recently from 3a87ab4 to 3aa16b0 Compare August 12, 2025 11:27
Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good, but there are still a lot of unrelated/unnecessary changes in the diff. Please revert them. This branch also needs a rebase to allow CI to pass.

@@ -539,6 +539,7 @@ impl Bolt11Payment {
} else {
None
};

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Spurious newline.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, noted

@@ -221,40 +221,40 @@ impl StorableObject for PaymentDetails {
},
}
}
if let Some(preimage_opt) = update.preimage {
if let Some(preimage_opt) = &update.preimage {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why these changes taking-by-reference were introduced, but I don't think we don't need any of them, especially if they'd require us to unnecessarily clone values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have fixed it.

@aagbotemi aagbotemi force-pushed the support-preimage-spontaneous-payment branch from 3aa16b0 to 2807540 Compare August 12, 2025 21:58
@aagbotemi aagbotemi force-pushed the support-preimage-spontaneous-payment branch from 2807540 to 90cde61 Compare August 12, 2025 22:13
@aagbotemi
Copy link
Contributor Author

Generally looks good, but there are still a lot of unrelated/unnecessary changes in the diff. Please revert them. This branch also needs a rebase to allow CI to pass.

I have fixed the unrelated changed, and reverted where necessary. Thank you.

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM

@tnull tnull merged commit a147ad0 into lightningdevkit:main Aug 13, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support passing preimage for spontaneous payments
6 participants